Skip to content

[BugFix] Fix json_set crash and json_delete no-op with $.key paths (#5167)#5339

Merged
qianheng-aws merged 1 commit into
opensearch-project:mainfrom
qianheng-aws:worktree-agent-a9339de0
Apr 14, 2026
Merged

[BugFix] Fix json_set crash and json_delete no-op with $.key paths (#5167)#5339
qianheng-aws merged 1 commit into
opensearch-project:mainfrom
qianheng-aws:worktree-agent-a9339de0

Conversation

@qianheng-aws

@qianheng-aws qianheng-aws commented Apr 13, 2026

Copy link
Copy Markdown
Collaborator

Description

json_set crashes (HTTP 500) and json_delete silently returns unchanged JSON when using standard JSONPath $.key syntax. The root cause is convertToJsonPath() in JsonUtils.java, which unconditionally prepends $. to the input — turning $.name into $.$.name (double-prefixed).

The fix strips any existing $. or $ prefix from the input before processing, preventing double-prefixing while preserving the existing curly-brace-to-bracket conversion logic.

Related Issues

Resolves #5167, #5170

Check List

  • New functionality includes testing
  • Commits signed per DCO (-s)
  • spotlessCheck passed
  • Unit tests passed
  • Integration tests passed

…pensearch-project#5167)

convertToJsonPath() unconditionally prepends "$." to input paths.
When users pass standard JSONPath syntax like "$.name", the path becomes
"$.$.name" (double-prefixed), causing json_set to crash with HTTP 500
and json_delete to silently return unchanged JSON.

Strip any existing "$." or "$" prefix before processing to prevent
double-prefixing while preserving the curly-brace-to-bracket conversion.

Signed-off-by: Heng Qian <qianheng@amazon.com>
@qianheng-aws

Copy link
Copy Markdown
Collaborator Author

Decision Log

Root Cause: JsonUtils.convertToJsonPath() unconditionally prepends $. to its input. When users pass standard JSONPath syntax like $.name, the result is $.$.name (double-prefixed). For json_set, Calcite's JsonFunctions.jsonSet interprets the invalid $. segment as a key and crashes. For json_delete, JsonFunctions.jsonRemove finds no match for $.$.name and returns the JSON unchanged.

Approach: Strip any existing $. or $ prefix from the input at the start of convertToJsonPath() before the function proceeds with its normal processing. This is the minimal change that handles all prefix variants ($, $., or none) and preserves the existing curly-brace-to-bracket conversion logic (e.g., {}[*]).

Alternatives Rejected:

  • Return early if input starts with $: This would skip the curly-brace conversion, breaking hypothetical inputs like $.a{}.b that combine both syntaxes. The strip-then-process approach is more robust.
  • Regex-based normalization: More complex for no additional benefit; the simple prefix check is clearer and handles all cases.

Pitfalls: None encountered — the fix is localized to one utility method. All existing tests (unit + integration + YAML) continue to pass.

Things to Watch: The json_extract, json_append, and json_extend functions also call convertToJsonPath, so they all benefit from this fix. Users can now use either $.name or name interchangeably for path arguments in all JSON mutation functions.

@github-actions

Copy link
Copy Markdown
Contributor

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

🧪 PR contains tests
🔒 No security concerns identified
✅ No TODO sections
🔀 No multiple PR themes
⚡ Recommended focus areas for review

Bracket Notation Handling

When stripping the $. prefix and the remaining input already contains bracket notation (e.g., $.[*] becomes [*]), the code then prepends $. again via StringBuilder sb = new StringBuilder("$."), resulting in $.[*]. This works correctly, but paths like $[0] (no dot after $) would become [0] and then $.[0], which changes the semantics slightly. The test covers $.[*] but not $[0] style paths. Verify that all valid JSONPath bracket-notation variants are handled correctly.

if (input.startsWith("$.")) {
  input = input.substring(2);
} else if (input.startsWith("$")) {
  input = input.substring(1);
}

if (input.isEmpty()) return "$";

StringBuilder sb = new StringBuilder("$.");
Already-Converted Paths

If a caller passes a path that has already been fully converted (e.g., $.a[2].c), the fix strips $. and re-processes a[2].c through the StringBuilder loop. The loop may not correctly handle bracket notation like [2] that is already in bracket form (not curly-brace form), potentially corrupting the path. Validate that pre-converted paths with mixed bracket/dot notation round-trip correctly through convertToJsonPath.

if (input.startsWith("$.")) {
  input = input.substring(2);
} else if (input.startsWith("$")) {
  input = input.substring(1);
}

if (input.isEmpty()) return "$";

StringBuilder sb = new StringBuilder("$.");

@github-actions

Copy link
Copy Markdown
Contributor

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
Possible issue
Verify correct argument type for delete eval call

The test calls JsonDeleteFunctionImpl.eval(...) with a String as the second
argument, but if the actual method signature expects a List or varargs of paths,
this may cause a compilation error or incorrect behavior at runtime. Verify that the
method signature of JsonDeleteFunctionImpl.eval matches the arguments being passed
in the test.

core/src/test/java/org/opensearch/sql/expression/json/JsonFunctionsTest.java [127-132]

 void test_jsonDeleteWithDollarPrefixedPath() throws Exception {
   // Issue #5167: json_delete with $.key path should remove the key
   Object result =
-      JsonDeleteFunctionImpl.eval("{\"name\":\"alice\",\"scores\":[90,85,92]}", "$.name");
+      JsonDeleteFunctionImpl.eval("{\"name\":\"alice\",\"scores\":[90,85,92]}", List.of("$.name"));
   assertEquals("{\"scores\":[90,85,92]}", result);
 }
Suggestion importance[1-10]: 5

__

Why: The suggestion raises a valid concern about whether JsonDeleteFunctionImpl.eval accepts a String or a List as the second argument. If the method signature expects a List, the test would fail to compile. However, the PR also adds a similar JsonSetFunctionImpl.eval call with a String argument that appears consistent, suggesting the API may accept strings directly.

Low
General
Handle edge case for $[n] style paths

After stripping the $. prefix, if the remaining string starts with [ (e.g., $.[]
becomes [
]), prepending $. will produce $.[*] which is correct. However, if the
input is $.a[2].c, stripping $. gives a[2].c, which is then processed by the loop
and will produce $.a[2].c correctly. But if the input is $[0] (no dot after $),
stripping $ gives [0], and the loop will prepend $. to produce $.[0]. This may be
inconsistent with the original intent. Consider verifying that the stripping logic
handles all edge cases like $[0] correctly, or add a test for this case.

core/src/main/java/org/opensearch/sql/expression/function/jsonUDF/JsonUtils.java [27-35]

 if (input.startsWith("$.")) {
   input = input.substring(2);
+} else if (input.startsWith("$[")) {
+  // e.g. "$[0]" -> "[0]", keep the bracket for the loop to handle
+  input = input.substring(1);
 } else if (input.startsWith("$")) {
   input = input.substring(1);
 }
 
 if (input.isEmpty()) return "$";
 
 StringBuilder sb = new StringBuilder("$.");
Suggestion importance[1-10]: 4

__

Why: The suggestion raises a valid edge case for $[0] style paths (without a dot after $), but the current code already handles this via the else if (input.startsWith("$")) branch, stripping just $ and leaving [0]. The loop then prepends $. to produce $.[0], which may or may not be the desired behavior. The suggestion adds a separate branch for $[ but the behavior is identical to the existing else if branch, making the improvement marginal.

Low

@songkant-aws

Copy link
Copy Markdown
Collaborator

Nit: Add two more regression test cases for json_append and json_extend as the fix also covers fix in #5170

Comment on lines +27 to +31
if (input.startsWith("$.")) {
input = input.substring(2);
} else if (input.startsWith("$")) {
input = input.substring(1);
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could the input start with other invalid char? for example $$.name, etc.

@qianheng-aws qianheng-aws Apr 14, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the minimal change that handles all prefix variants (\$, \$., or none)

No, $$. is not standard JSONPath.

@qianheng-aws qianheng-aws merged commit 175d199 into opensearch-project:main Apr 14, 2026
42 of 43 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working bugFix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] json_set crashes and json_delete is no-op due to JSONPath double-prefixing

3 participants